/*------------------------------------------------------------------------------
* GeoVISTA Center, Penn State Geography Deptartment*
* Copyright (c), 2002, GeoVISTA Center
* All Rights Researved.
*
* Original Authors: Frank Hardisty, Bonan Li
* $Author: bonan_li $
*
* $Date: 2004/03/03 19:03:11 $
*
* $Id: LayerShapeExtension.java,v 1.3 2004/03/03 19:03:11 bonan_li Exp $
*
* To Do:
*
 ------------------------------------------------------------------------------*/


package edu.psu.geovista.app.map;

//import edu.psu.geovista.data.geog.DataSetForApps;

import edu.psu.geovista.ui.Fisheyes;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Vector;

/**
 * Layer and its subclasses are responsible for rendering spatial data,
 * using classifications and symbolizations set by the user.
 *
 * The spatial data to be rendered is expected to be in user device space.
 */
public abstract class LayerShapeExtension {
  public static String COMMAND_SELECTION = "cmdSel";
  public static int STATUS_NOT_SELECTED = 0; //default
  public static int STATUS_SELECTED = 1;
  public static final int LAYER_TYPE_POINT = 0;
  public static final int LAYER_TYPE_LINE = 1;
  public static final int LAYER_TYPE_POLYGON = 2;
  public static final int LAYER_TYPE_RASTER = 3;
  public static final int LAYER_TYPE_SYMBOL = 4;
  public static final int FILL_ORDER_MAX = 3;
  private transient Rectangle extent;
  protected transient Shape[] spatialData; //in user space
  protected transient Shape[] originalSpatialData; //originalCoordinates
  protected transient Rectangle[] boundingBoxes;
  protected transient int indication;
  protected transient AffineTransform xform;
  protected transient int[] classification;
  protected transient int[] focus;
  protected transient int[] selectedObservations;
  protected transient int[] selectedObservationsFullIndex;
  protected transient int[] selectedObservationsOld;
  protected transient int[] selectedObservationsOldFullIndex;
  protected transient Color[] objectColors;
  protected transient double[][] data; //column, row
  protected transient String[] variableNames;
  protected transient BufferedImage buff;
  protected transient int currOrderColumn;
  protected transient int currColorColumn;
  protected transient int[] conditionArray;

  //Colors
  //protected Color colorSelection = new Color(Color.blue.getRed(),Color.blue.getGreen(),Color.blue.getBlue(),128);
  protected transient Color colorSelection = Color.blue;
  protected transient Color colorIndication = new Color(Color.green.getRed(),
      Color.green.getGreen(),
      Color.green.getBlue(), 150);

  //protected Color colorIndication = Color.green;
  protected transient Color colorNull = Color.darkGray;
  protected transient Color colorOutOfFocus = Color.black;
  protected transient Color colorNotInStudyArea = Color.black;
  protected transient Color colorLine = Color.black;
  protected transient Color colorAuxLine = Color.darkGray;
  protected transient Color colorBackground = Color.white;
  protected transient float defaultStrokeWidth;
  protected transient Stroke defaultStroke;
  protected transient Stroke selectionStroke;
  protected transient Stroke deselectionStroke;
  protected transient TexturePaint selectionTexture;
  protected transient TexturePaint indicationTexture;
  protected boolean isAuxiliary = false;
  protected transient float[] spatialDataArea; //area of spatial data in pixels
  transient Fisheyes fisheyes;
  transient boolean colorsRecieved = false;
  transient boolean selectionExists = false;
  transient boolean selOriginalColorMode = true;

  public LayerShapeExtension() {
    this.indication = Integer.MIN_VALUE;
    this.selectedObservations = new int[0];
    this.xform = new AffineTransform();
    defaultStroke = new BasicStroke(1f);
    selectionStroke = new BasicStroke(2f);
    deselectionStroke = new BasicStroke(2f);
    selectedObservationsOld = new int[0];
    makeTextures();
  }

  private void makeTextures() {
    int texSize = 4;

    Rectangle2D.Float indRect = new Rectangle2D.Float(0, 0, texSize, texSize);
    BufferedImage indBuff = new BufferedImage(texSize, texSize,
                                              BufferedImage.TYPE_INT_ARGB);
    Graphics2D indG2 = indBuff.createGraphics();
    indG2.setColor(this.colorIndication);
    indG2.drawLine(0, texSize, texSize, 0);
    this.indicationTexture = new TexturePaint(indBuff, indRect);

    Rectangle2D.Float selRect = new Rectangle2D.Float(0, 0, texSize, texSize);
    BufferedImage selBuff = new BufferedImage(texSize, texSize,
                                              BufferedImage.TYPE_INT_ARGB);
    Graphics2D selG2 = selBuff.createGraphics();
    if (this.selOriginalColorMode) {
      selG2.setColor(this.colorBackground);
    }
    else {
      selG2.setColor(this.colorSelection);
    }

    BasicStroke doubleWide = new BasicStroke(2f);

    selG2.setStroke(doubleWide);
    selG2.drawLine(0, 0, texSize, texSize);
    //%%%%%%%%%%%%%%%%%%%%
    //selG2.setColor(this.colorSelection);
    //selG2.fill(new Rectangle(selBuff.getWidth(),selBuff.getHeight()));
    //%%%%%%%%%%%%%%%%%%%%%%
    this.selectionTexture = new TexturePaint(selBuff, selRect);
  }

  private void initColors() {
    this.objectColors = new Color[spatialData.length];

    for (int i = 0; i < objectColors.length; i++) {
      objectColors[i] = Color.yellow;
    }

    this.conditionArray = new int[spatialData.length];
  }

  public Point findCentroid(int obs) {

    Shape s = this.spatialData[obs];

    //here's an inaccurate but fast algorithm. To do: replace with something
    //more accurate but also fast.
    Rectangle rect = s.getBounds();
    int x = (int) (rect.getX() + rect.getWidth() / 2d);
    int y = (int) (rect.getY() + rect.getHeight() / 2d);
    Point p = new Point(x, y);

    return p;
  }

  //begin accessors
  public void setExtent(Rectangle extent) {
    this.extent = extent;
  }

  //MapCanvas calls this on resize and data being set
  public void setSpatialData(Shape[] spatialData) {
    /*
         //special stuff for Chaomei
         if (this.isAuxiliary) {
      //String[] observationNames = dataSet.getObservationNames();
      int[] numPoints = new int[25];
      int x = 0;
      int y = 0;
      int prevX = 0;
      int prevY = 0;
      int whichShape = 0;
      int numSkipped = 0;
      System.out.println(spatialData.length);
      for (int i = 0; i < spatialData.length; i++) {
        //System.out.println(i);
        //special thing: if points are identical to previous points, we skip 'em
         PathIterator pi = spatialData[i].getPathIterator(new AffineTransform());
        int numPolys = 0;
        while (!pi.isDone()) {
          float[] coords = new float[6];
          int segType = pi.currentSegment(coords);
          x = (int)coords[0];
          y = (int)coords[1];
          if (segType == PathIterator.SEG_MOVETO) {
            x = Integer.MAX_VALUE;
            y = Integer.MAX_VALUE;
            numPolys++;
            numPoints[numPolys] = 0;
          }
          if (x == prevX && y == prevY) {
            numSkipped++;
          } else {
            numPoints[numPolys]++;
            prevX = x;
            prevY = y;
          }
          pi.next();
        }//wend
        pi = spatialData[i].getPathIterator(new AffineTransform());
        //System.out.println(observationNames[i]);
        System.out.println(numPolys);
        numPolys = 0;
        while (!pi.isDone()) {
          float[] coords = new float[6];
          int segType = pi.currentSegment(coords);
          x = (int)coords[0];
          y = (int)coords[1];
          if (segType == PathIterator.SEG_MOVETO) {
            x = Integer.MAX_VALUE;
            y = Integer.MAX_VALUE;
            numPolys++;
            System.out.println(numPoints[numPolys]);
          }
          if (x == prevX && y == prevY) {
            numSkipped++;
          } else {
            System.out.println(coords[0] +","+ coords[1]);
            prevX = x;
            prevY = y;
          }
          pi.next();
        }//wend
      } //next spatialData
      System.out.println("num skipped " + numSkipped);
         } //end if aux
         //end special stuff for chaomei*/
    this.spatialData = spatialData;

    //we need to check for null if this is the first time through
    //we need to check for length in case the spatial data
    //has changed.
    //however the spatial data might well change with no other changes
    //needed, in case of panning or zooming, for instance.
    int numObs = spatialData.length;

    if ( (classification == null) || (classification.length != numObs)) {
      classification = new int[spatialData.length];
    }

    if ( (objectColors == null) || (this.objectColors.length != numObs)) {
      this.initColors();
    }

    if ( (selectedObservationsFullIndex == null) ||
        (selectedObservationsFullIndex.length != numObs)) {
      selectedObservationsFullIndex = new int[spatialData.length];
    }

    if ( (selectedObservationsOldFullIndex == null) ||
        (selectedObservationsOldFullIndex.length != numObs)) {
      selectedObservationsOldFullIndex = new int[spatialData.length];
    }

    if ( (conditionArray == null) || (conditionArray.length != numObs)) {
      conditionArray = new int[numObs];
    }

    if ( (spatialDataArea == null) || (spatialDataArea.length != numObs)) {
      spatialDataArea = new float[numObs];
    }
  }

  public Shape[] getSpatialData() {
    return this.spatialData;
  }

  public Color[] getColors() {
    return this.objectColors;
  }

  public void setBoundingBoxes(Rectangle[] boundingBoxes) {
    this.boundingBoxes = boundingBoxes;
  }

  public void setIndication(int indication) {
    this.indication = indication;
  }

  public void setXform(AffineTransform xform) {
    this.xform = xform;
  }

  public void setClassification(int[] classification) {
    this.classification = classification;
  }

  public void setFocus(int[] focus) {
    this.focus = focus;
  }

  //just sets data
  public void setSelectedObservations(int[] selectedObservations) {
    //copy old full index values
    for (int i = 0; i < selectedObservationsFullIndex.length; i++) {
      selectedObservationsOldFullIndex[i] = selectedObservationsFullIndex[i];
    }

    //reset full index array
    for (int i = 0; i < selectedObservationsFullIndex.length; i++) {
      selectedObservationsFullIndex[i] = this.STATUS_NOT_SELECTED;
    }

    //set correct selection values in full index
    for (int i = 0; i < selectedObservations.length; i++) {
      int obs = selectedObservations[i];
      selectedObservationsFullIndex[obs] = this.STATUS_SELECTED;
    }
    //set selectionExists for rendering
    if (selectedObservations.length > 0) {
      this.selectionExists = true;
    }
    else {
      this.selectionExists = false;
    }

    this.selectedObservations = selectedObservations; //this happens anyway

    //copy current selection values to the old one.
    if (selectedObservationsOld.length != selectedObservations.length) {
      selectedObservationsOld = new int[selectedObservations.length];
    }

    for (int i = 0; i < selectedObservations.length; i++) {
      selectedObservationsOld[i] = selectedObservations[i];

    }
  }

//  //paints as well as sets data
//  public void setSelectedObservations(int[] selectedObservations, Graphics2D g2) {
//    //if (this.selectedObservations == selectedObservations) {
//    //  return;
//    //}
//    //copy old full index values
//    for (int i = 0; i < selectedObservationsFullIndex.length; i++) {
//      selectedObservationsOldFullIndex[i] = selectedObservationsFullIndex[i];
//    }
//
//    //reset full index array
//    for (int i = 0; i < selectedObservationsFullIndex.length; i++) {
//      selectedObservationsFullIndex[i] = this.STATUS_NOT_SELECTED;
//    }
//
//    //set correct selection values in full index
//    for (int i = 0; i < selectedObservations.length; i++) {
//      int obs = selectedObservations[i];
//      selectedObservationsFullIndex[obs] = this.STATUS_SELECTED;
//    }
//    //set selectionExists for rendering
//    if (selectedObservations.length > 0) {
//      this.selectionExists = true;
//    }
//    else {
//      this.selectionExists = false;
//    }
//
//    this.selectedObservations = selectedObservations; //this happens anyway
//
//    //now we render.
//    //step one: paint observations that were selected but now are not.
//    for (int i = 0; i < selectedObservationsOld.length; i++) {
//      int obs = selectedObservationsOld[i];
//
//      if (selectedObservationsFullIndex[obs] != this.STATUS_SELECTED) {
//        this.renderObservationNoIndication(obs, g2);
//      } //end if
//    } //next obs
//
//    //step two: paint observations that are selected
//    for (int i = 0; i < selectedObservations.length; i++) {
//      int obs = selectedObservations[i];
//
//      //if (selectedObservationsOldFullIndex[obs] != this.STATUS_SELECTED) {
//      this.renderObservationNoIndication(obs, g2);
//
//      //}//end if
//    } //next obs
//
//    //copy current selection values to the old one.
//    if (selectedObservationsOld.length != selectedObservations.length) {
//      selectedObservationsOld = new int[selectedObservations.length];
//    }
//
//    for (int i = 0; i < selectedObservations.length; i++) {
//      selectedObservationsOld[i] = selectedObservations[i];
//    }
//
//  }

  public int[] getSelectedObservations() {
    return this.selectedObservations;
  }

  public void setIsAuxiliary(boolean isAuxiliary) {
    this.isAuxiliary = isAuxiliary;

    int red = Color.darkGray.getRed();
    int green = Color.darkGray.getGreen();
    int blue = Color.darkGray.getBlue();
    int alpha = 200;
    Color transGray = new Color(red, green, blue, alpha);
    this.colorLine = transGray;
    this.defaultStroke = new BasicStroke(2f);
  }

  public void setParentSize(int height, int width) {
    int shapeCount = 0;
    Rectangle currBox = new Rectangle(0, 0, width, height);

    for (int i = 0; i < this.spatialData.length; i++) {
      if (currBox.intersects(spatialData[i].getBounds())) {
        shapeCount++;
      }
    }

    if (shapeCount <= 0) {
      return;
    }

    this.spatialDataArea = new float[shapeCount];

    int counter = 0;

    for (int i = 0; i < spatialData.length; i++) {
      if (currBox.intersects(spatialData[i].getBounds2D())) {
        Rectangle rect = spatialData[i].getBounds();

        float area = (float) spatialData[i].getBounds().getWidth() *
            (float) spatialData[i].getBounds()
            .getHeight();
        spatialDataArea[counter] = area;

        //System.out.println("area = " + area);
        counter++;
      }
    }

    Arrays.sort(spatialDataArea);

    int firstForth = spatialDataArea.length / 2;
    float shapeArea = spatialDataArea[firstForth];

    //System.out.println("LayerShape.setParent, shapeCount = " + shapeCount);
    //System.out.println(",shapeArea = " + shapeArea);
    float strokeWidth = 0f;

    if (shapeArea < 40) {
      strokeWidth = 0f;
    }
    else if (shapeArea < 2500) {
      strokeWidth = 0.1f;
    }
    else if (shapeArea < 5000) {
      strokeWidth = 1f;
    }
    else if (shapeArea < 500000) {
      strokeWidth = 2f;
    }
    else {
      strokeWidth = 3f;
    }
    //if (strokeWidth > 3f) strokeWidth = 3f;//max size
    //System.out.println("strokeWidth = " + strokeWidth);
    //System.out.println();
    this.defaultStroke = new BasicStroke(strokeWidth);

    //this.defaultStroke = new BasicStroke(strokeWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND);
    this.defaultStrokeWidth = strokeWidth;
    if (strokeWidth < 1) { //start with at least one for finding selectionStroke
      strokeWidth = 1;
    }
    this.selectionStroke = new BasicStroke(strokeWidth * 1.5f,
                                           BasicStroke.CAP_ROUND,
                                           BasicStroke.JOIN_ROUND);
  }

  public void setOriginalSpatialData(Shape[] spatialData) {
    this.originalSpatialData = spatialData;
  }

  public Shape[] getOriginalSpatialData() {
    return this.originalSpatialData;
  }

  public boolean getIsAuxiliary() {
    return this.isAuxiliary;
  }

  public void setObjectColors(Color[] objectColors) {
    this.objectColors = objectColors;
    this.colorsRecieved = true;
  }

  public void setData(double[][] data) {
    this.data = data;
  }

  public void setVariableNames(String[] variableNames) {
    this.variableNames = variableNames;
  }

  public void setCurrOrderColumn(int currOrderColumn) {
    this.currOrderColumn = currOrderColumn;
  }

  public void setCurrColorColumn(int currColorColumn) {
    this.currColorColumn = currColorColumn;
  }

  public void setColorSelection(Color colorSelection) {
    this.colorSelection = colorSelection;
    this.makeTextures();
  }

  public void setColorIndication(Color colorIndication) {
    this.colorIndication = colorIndication;
    this.makeTextures();
  }

  public void setColorNull(Color colorNull) {
    this.colorNull = colorNull;
  }

  public void setColorOutOfFocus(Color colorOutOfFocus) {
    this.colorOutOfFocus = colorOutOfFocus;
  }

  public void setColorNotInStudyArea(Color colorNotInStudyArea) {
    this.colorNotInStudyArea = colorNotInStudyArea;
  }

  /**
   * put your documentation comment here
   * @param conditionArray
   */
  public void setConditionArray(int[] conditionArray) {
    this.conditionArray = conditionArray;
  }

  /**
   * put your documentation comment here
   * @param conditionArray
   */
  public int[] getConditionArray() {
    return this.conditionArray;
  }

  public void setSelOriginalColorMode(boolean selOriginalColorMode) {
    this.selOriginalColorMode = selOriginalColorMode;
    this.makeTextures();
  }

  //end accessors
  // Abstract methods that must be implemented by a subclass
  public abstract void findSelection(int x1, int x2, int y1, int y2);

  public abstract void findSelectionShift(int x1, int x2, int y1, int y2);

  public abstract int findIndication(int x, int y);

  /*
   * SelectionX1 is expected to be less than selectionX2, same with Y1 and y2.
   * Selected observations should be rendered with the color "colorSelection".
   */
  public int[] findSelection(Rectangle2D selBox) {
    //Rectangle selBox = new Rectangle(selectionX1,selectionY1,selectionX2-selectionX1,selectionY2-selectionY1);

    Vector selObs = new Vector();
    for (int i = 0; i < this.spatialData.length; i++) {
      Rectangle shpBox = this.spatialData[i].getBounds();
      Rectangle2D r2 = null;
      if (selBox.intersects(shpBox)) {
        if (this.spatialData[i].contains(selBox) ||
            this.spatialData[i].intersects(selBox)) {
          selObs.add(new Integer(i));
        } //end if really intersects
      } //end if rough intersects
    } //next
    int[] selObsInt = new int[selObs.size()];
    int j = 0;
    for (Enumeration e = selObs.elements(); e.hasMoreElements(); ) {
      Integer anInt = (Integer) e.nextElement();
      selObsInt[j] = anInt.intValue();
      j++;
    }
    return selObsInt;
  }

  public String toString() {
    String s = this.getClass().toString();

    if (this.spatialData == null) {
      s = s + ", spatialData == null.";
    }
    else {
      s = s + ", spatialData.length = " + spatialData.length + ".";
    }

    return s;
  }

//  public void renderObservation(int obs, Graphics2D g2)
//
//  {
//System.out.println("renderObservation......LayerShape.");
//    if (obs < 0) {
//      return;
//    }
//    Shape shp = spatialData[obs];
//    if (fisheyes != null) {
//      shp = fisheyes.transform(shp);
//    }
//    if (conditionArray[obs] > -1) {
//      g2.setStroke(this.defaultStroke);
//
//      Color color = this.objectColors[obs];
//      g2.setColor(color);
//      g2.fill(shp);
//      g2.setColor(Color.black);
//      g2.draw(shp);
//      if (this.selOriginalColorMode) {
//        if (this.selectedObservationsFullIndex[obs] == this.STATUS_NOT_SELECTED &&
//            this.selectionExists) {
//          g2.setPaint(this.selectionTexture);
//
//          g2.fill(shp);
//        }
//      }
//        else if (!this.selOriginalColorMode) {
//          if (this.selectedObservationsFullIndex[obs] == this.STATUS_SELECTED) {
//            g2.setPaint(this.selectionTexture);
//
//            g2.fill(shp);
//          }
//
//
//
//      }
//
//
//    } //end if condition
//
//    if (obs == indication) {
//      Stroke tempStroke = g2.getStroke();
//      g2.setStroke(this.selectionStroke);
//      g2.setColor(this.colorIndication);
//      g2.draw(shp);
//      if (fisheyes == null) {
//        g2.setStroke(tempStroke);
//        g2.setPaint(this.indicationTexture);
//        g2.fill(shp);
//      }
//
//    }
//  }


//  public void renderObservationNoIndication(int obs, Graphics2D g2)
//  {
//    if (obs < 0) {
//      return;
//    }
//    if (this.objectColors.length <= obs) {
//      return;
//    }
//    Shape shp = spatialData[obs];
//    if (fisheyes != null) {
//      shp = fisheyes.transform(shp);
//    }
//
//    if (conditionArray[obs] > -1) {
//      g2.setStroke(this.defaultStroke);
//      if(!this.selOriginalColorMode || this.selectedObservationsFullIndex[obs] == this.STATUS_SELECTED ||!this.selectionExists){
//        Color color = this.objectColors[obs];
//        g2.setColor(color);
//        g2.fill(shp);
//      }
//
//      if (this.defaultStrokeWidth >= 0.1f) {
//        g2.setColor(this.colorLine);
//        g2.draw(shp);
//      }
//
//      if (this.selOriginalColorMode) {
//        if (this.selectedObservationsFullIndex[obs] == this.STATUS_NOT_SELECTED &&
//            this.selectionExists) {
//          g2.setPaint(this.selectionTexture);
//
//          //g2.fill(shp);
//        }
//      }
//        else if (!this.selOriginalColorMode) {
//          if (this.selectedObservationsFullIndex[obs] == this.STATUS_SELECTED) {
//            g2.setPaint(this.selectionTexture);
//
//            g2.fill(shp);
//          }
//      }
//
//    } //end if condition
//    //System.out.println("selOriginal = " + this.selOriginalColorMode);
//  }

  private int paintTimes;
 // public void render(Graphics2D g2);
//  {
//
//    if (this.objectColors == null) {
//      System.out.println("LayerShape, render called on null objectColors");
//      return;
//    }
//
//    if (g2 == null) {
//      throw new IllegalArgumentException(this.toString() +
//          " Null graphics passed in to render(Graphics2D).");
//    }
//
//    if (this.isAuxiliary) {
//      this.renderAux(g2);
//
//      return;
//    }
//
//    for (int path = 0; path < spatialData.length; path++) {
//      this.renderObservationNoIndication(path, g2);
//    }
//  } //end method

  protected void renderAux(Graphics2D g2) {
    //draw all shapes
    RenderingHints qualityHints = new RenderingHints(null);

    qualityHints.put(RenderingHints.KEY_ANTIALIASING,
                     RenderingHints.VALUE_ANTIALIAS_ON);

    qualityHints.put(RenderingHints.KEY_RENDERING,
                     RenderingHints.VALUE_RENDER_QUALITY);

    g2.setRenderingHints(qualityHints);
    //if (this.defaultStrokeWidth >= 0.1f) {
    g2.setColor(this.colorAuxLine);
    g2.setStroke(this.selectionStroke);

    if (spatialData != null) {
      int numPaths = spatialData.length;
       System.out.println("draw auxiliary data...." + numPaths);
      for (int path = 0; path < numPaths; path++) {
        Shape s = spatialData[path];
        if (fisheyes != null) {
          s = fisheyes.transform(s);
        }
        g2.draw(s);
      } // end for path
    } // end if null
    //} //end if stroke width > 1
  }

  public Color getColorBackground() {
    return colorBackground;
  }

  public void setColorBackground(Color colorBackground) {
    this.colorBackground = colorBackground;
    int rgb = colorBackground.getRed() + colorBackground.getGreen() + colorBackground.getBlue();
    //System.out.println("rgb " + rgb);
    if (rgb > 300){
      //its close to white, so
      this.colorLine = Color.lightGray;
    }else
      this.colorLine = Color.darkGray;
    this.makeTextures();
  }
}